查看原文
其他

    题外话:欢迎将公众号设置为星标,技术文章第一时间看到。我们将一如既往精选技术好文,提供有价值的阅读。如有读者想要投稿,可以在公众号任意文章下留言,技术博主奖励丰厚。

    1. Java 性能优化:教你提高代码运行的效率

    2. Java问题排查工具清单

    3. 记住:永远不要在MySQL中使用UTF-8

    4. Springboot启动原理解析

    本文主要介绍如何通过netty来手写一套简单版的HTTP服务器,同时将关于netty的许多细小知识点进行了串联,用于巩固和提升对于netty框架的掌握程度。

    服务器运行效果

    服务器支持对静态文件css,js,html,图片资源的访问。通过网络的形式对这些文件可以进行访问,相应截图如下所示:


    支持对于js,css,html等文件的访问:




    然后引用相应的pom依赖文件信息:


            <dependency>
               <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.47</version>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>

            <dependency>
                <groupId>io.netty</groupId>
                <artifactId>netty-all</artifactId>
                <version>4.1.6.Final</version>
            </dependency>

            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.13</version>
            </dependency>

            <dependency>
                <groupId>cglib</groupId>
                <artifactId>cglib</artifactId>
                <version>3.2.6</version>
            </dependency>

    导入依赖之后,新建一个包itree.demo(包名可以自己随便定义)

    定义一个启动类WebApplication.java(有点类似于springboot的那种思路)

    package itree.demo;

    import com.sise.itree.ITreeApplication;

    /**
     * @author idea
     * @data 2019/4/30
     */

    public class WebApplication {

        public static void main(String[] args) throws IllegalAccessException, InstantiationException {
            ITreeApplication.start(WebApplication.class);
        }
    }

    在和这个启动类同级别的包底下,建立itree.demo.controller和itree.demo.filter包,主要是用于做测试:

    建立一个测试使用的Controller:

    package itree.demo.controller;

    import com.sise.itree.common.BaseController;
    import com.sise.itree.common.annotation.ControllerMapping;
    import com.sise.itree.core.handle.response.BaseResponse;
    import com.sise.itree.model.ControllerRequest;

    /**
     * @author idea
     * @data 2019/4/30
     */

    @ControllerMapping(url = "/myController")
    public class MyController implements BaseController {

        @Override
        public BaseResponse doGet(ControllerRequest controllerRequest) {
            String username= (String) controllerRequest.getParameter("username");
            System.out.println(username);
            return new BaseResponse(1,username);
        }

        @Override
        public BaseResponse doPost(ControllerRequest controllerRequest) {
            return null;
        }
    }

    这里面的BaseController是我自己在Itree包里面编写的接口,这里面的格式有点类似于javaee的servlet,之前我在编写代码的时候有点参考了servlet的设计。(注解里面的url正是匹配了客户端访问时候所映射的url链接)

    编写相应的过滤器:

    package itree.demo.filter;

    import com.sise.itree.common.BaseFilter;
    import com.sise.itree.common.annotation.Filter;
    import com.sise.itree.model.ControllerRequest;

    /**
     * @author idea
     * @data 2019/4/30
     */

    @Filter(order = 1)
    public class MyFilter implements BaseFilter {

        @Override
        public void beforeFilter(ControllerRequest controllerRequest) {
            System.out.println("before");
        }

        @Override
        public void afterFilter(ControllerRequest controllerRequest) {
            System.out.println("after");
        }
    }

    通过代码的表面意思,可以很好的理解这里大致的含义。当然,如果过滤器有优先顺序的话,可以通过@Filter注解里面的order属性进行排序。搭建起多个controller和filter之后,整体项目的结构如下所示:


    基础的java程序写好之后,便是相应的resources文件了:
    这里提供了可适配性的配置文件,默认配置文件命名为resources的config/itree-config.properties文件:


    暂时可提供的配置有以下几个:


    server.port=9090
    index.page=html/home.html
    not.found.page=html/404.html

    结合相应的静态文件放入之后,整体的项目结构图如下所示:


    这个时候可以启动之前编写的WebApplication启动类


    启动的时候控制台会打印出相应的信息:

    启动类会扫描同级目录底下所有带有@Filter注解和@ControllerMapping注解的类,然后加入指定的容器当中。(这里借鉴了Spring里面的ioc容器的思想)

    启动之后,进行对于上述controller接口的访问测试,便可以查看到以下信息的内容:


    同样,我们查看控制台的信息打印:



    controller接收数据之前,通过了三层的filter进行过滤,而且过滤的顺序也是和我们之前预期所想的那样一直,按照order从小到大的顺序执行(同样我们可以接受post类型的请求)


    除了常规的接口类型数据响应之外,还提供有静态文件的访问功能:


    对于静态文件里面的html也可以通过网络url的形式来访问:


    home.html文件内容如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    this is home
    </body>
    </html>

    我们在之前说的properties文件里面提及了相应的初始化页面配置是:

    index.page=html/home.html

    因此,访问的时候默认的http://localhost:9090/就会跳转到该指定页面:


    假设不配置properties文件的话,则会采用默认的页面跳转,默认的端口号8080



    默认的404页面为



    基本的使用步骤大致如上述所示。

    那么又该怎么来进行这样的一套框架设计和编写呢?

    首先从整体设计方面,核心内容是分为了netty的server和serverHandler处理器:

    首先是接受数据的server端:

    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.handler.codec.http.HttpObjectAggregator;
    import io.netty.handler.codec.http.HttpRequestDecoder;
    import io.netty.handler.codec.http.HttpResponseEncoder;
    import io.netty.handler.stream.ChunkedWriteHandler;

    /**
     * @author idea
     * @data 2019/4/26
     */

    public class NettyHttpServer {

        private int inetPort;

        public NettyHttpServer(int inetPort) {
            this.inetPort = inetPort;
        }

        public int getInetPort() {
            return inetPort;
        }


        public void init() throws Exception {

            EventLoopGroup parentGroup = new NioEventLoopGroup();
            EventLoopGroup childGroup = new NioEventLoopGroup();

            try {
                ServerBootstrap server = new ServerBootstrap();
                // 1. 绑定两个线程组分别用来处理客户端通道的accept和读写时间
                server.group(parentGroup, childGroup)
                        // 2. 绑定服务端通道NioServerSocketChannel
                        .channel(NioServerSocketChannel.class)
                        // 3. 给读写事件的线程通道绑定handler去真正处理读写
                        // ChannelInitializer初始化通道SocketChannel
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                // 请求解码器
                                socketChannel.pipeline().addLast("http-decoder"new HttpRequestDecoder());
                                // 将HTTP消息的多个部分合成一条完整的HTTP消息
                                socketChannel.pipeline().addLast("http-aggregator"new HttpObjectAggregator(65535));
                                // 响应转码器
                                socketChannel.pipeline().addLast("http-encoder"new HttpResponseEncoder());
                                // 解决大码流的问题,ChunkedWriteHandler:向客户端发送HTML5文件
                                socketChannel.pipeline().addLast("http-chunked"new ChunkedWriteHandler());
                                // 自定义处理handler
                                socketChannel.pipeline().addLast("http-server"new NettyHttpServerHandler());
                            }
                        });

                // 4. 监听端口(服务器host和port端口),同步返回
                ChannelFuture future = server.bind(this.inetPort).sync();
                System.out.println("[server] opening in "+this.inetPort);
                // 当通道关闭时继续向后执行,这是一个阻塞方法
                future.channel().closeFuture().sync();
            } finally {
                childGroup.shutdownGracefully();
                parentGroup.shutdownGracefully();
            }
        }

    }

    Netty接收数据的处理器NettyHttpServerHandler 代码如下:

    import com.alibaba.fastjson.JSON;
    import com.sise.itree.common.BaseController;
    import com.sise.itree.model.ControllerRequest;
    import com.sise.itree.model.PicModel;
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelFutureListener;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.handler.codec.http.FullHttpRequest;
    import io.netty.handler.codec.http.FullHttpResponse;
    import io.netty.handler.codec.http.HttpMethod;
    import io.netty.handler.codec.http.HttpResponseStatus;
    import io.netty.util.CharsetUtil;
    import com.sise.itree.core.handle.StaticFileHandler;
    import com.sise.itree.core.handle.response.BaseResponse;
    import com.sise.itree.core.handle.response.ResponCoreHandle;
    import com.sise.itree.core.invoke.ControllerCglib;
    import lombok.extern.slf4j.Slf4j;

    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Map;

    import static io.netty.buffer.Unpooled.copiedBuffer;
    import static com.sise.itree.core.ParameterHandler.getHeaderData;
    import static com.sise.itree.core.handle.ControllerReactor.getClazzFromList;
    import static com.sise.itree.core.handle.FilterReactor.aftHandler;
    import static com.sise.itree.core.handle.FilterReactor.preHandler;
    import static com.sise.itree.util.CommonUtil.*;

    /**
     * @author idea
     * @data 2019/4/26
     */

    @Slf4j
    public class NettyHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws Exception {
            String uri = getUri(fullHttpRequest.getUri());
            Object object = getClazzFromList(uri);
            String result = "recive msg";
            Object response = null;

            //静态文件处理
            response = StaticFileHandler.responseHandle(object, ctx, fullHttpRequest);

            if (!(response instanceof FullHttpResponse) && !(response instanceof PicModel)) {

                //接口处理
                if (isContaionInterFace(object, BaseController.class)) {
                    ControllerCglib cc = new ControllerCglib();
                    Object proxyObj = cc.getTarget(object);
                    Method[] methodArr = null;
                    Method aimMethod = null;


                    if (fullHttpRequest.method().equals(HttpMethod.GET)) {
                        methodArr = proxyObj.getClass().getMethods();
                        aimMethod = getMethodByName(methodArr, "doGet");
                    } else if (fullHttpRequest.method().equals(HttpMethod.POST)) {
                        methodArr = proxyObj.getClass().getMethods();
                        aimMethod = getMethodByName(methodArr, "doPost");
                    }

                    //代理执行method
                    if (aimMethod != null) {
                        ControllerRequest controllerRequest=paramterHandler(fullHttpRequest);
                        preHandler(controllerRequest);
                        BaseResponse baseResponse = (BaseResponse) aimMethod.invoke(proxyObj, controllerRequest);
                        aftHandler(controllerRequest);
                        result = JSON.toJSONString(baseResponse);
                    }
                }
                response = ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);
            }
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }


        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
        }


        /**
         * 处理请求的参数内容
         *
         * @param fullHttpRequest
         * @return
         */

        private ControllerRequest paramterHandler(FullHttpRequest fullHttpRequest) {
            //参数处理部分内容
            Map<String, Object> paramMap = new HashMap<>(60);
            if (fullHttpRequest.method() == HttpMethod.GET) {
                paramMap = ParameterHandler.getGetParamsFromChannel(fullHttpRequest);
            } else if (fullHttpRequest.getMethod() == HttpMethod.POST) {
                paramMap = ParameterHandler.getPostParamsFromChannel(fullHttpRequest);
            }
            Map<String, String> headers = getHeaderData(fullHttpRequest);

            ControllerRequest ctr = new ControllerRequest();
            ctr.setParams(paramMap);
            ctr.setHeader(headers);
            return ctr;
        }


    }

    这里面的核心模块我大致分成了:

    url匹配处理:

    我们的客户端发送的url请求进入server端之后,需要快速的进行url路径的格式处理。例如将http://localhost:8080/xxx-1/xxx-2?username=test转换为/xxx-1/xxx-2的格式,这样方便和controller顶部设计的注解的url信息进行关键字匹配。

        /**
         * 截取url里面的路径字段信息
         *
         * @param uri
         * @return
         */

        public static String getUri(String uri) {
            int pathIndex = uri.indexOf("/");
            int requestIndex = uri.indexOf("?");
            String result;
            if (requestIndex < 0) {
                result = uri.trim().substring(pathIndex);
            } else {
                result = uri.trim().substring(pathIndex, requestIndex);
            }
            return result;
        }

    从容器获取匹配响应数据:

    经过了前一段的url格式处理之后,我们需要根据url的后缀来预先判断是否是数据静态文件的请求:

    对于不同后缀格式来返回不同的model对象(每个model对象都是共同的属性url),之所以设计成不同的对象是因为针对不同格式的数据,response的header里面需要设置不同的属性值。

        /**
         * 匹配响应信息
         *
         * @param uri
         * @return
         */

        public static Object getClazzFromList(String uri) {
            if (uri.equals("/") || uri.equalsIgnoreCase("/index")) {
                PageModel pageModel;
                if(ITreeConfig.INDEX_CHANGE){
                    pageModel= new PageModel();
                    pageModel.setPagePath(ITreeConfig.INDEX_PAGE);
                }
                return new PageModel();
            }
            if (uri.endsWith(RequestConstants.HTML_TYPE)) {
                return new PageModel(uri);
            }
            if (uri.endsWith(RequestConstants.JS_TYPE)) {
                return new JsModel(uri);
            }
            if (uri.endsWith(RequestConstants.CSS_TYPE)) {
                return new CssModel(uri);
            }
            if (isPicTypeMatch(uri)) {
                return new PicModel(uri);
            }

            //查看是否是匹配json格式
            Optional<ControllerMapping> cmOpt = CONTROLLER_LIST.stream().filter((p) -> p.getUrl().equals(uri)).findFirst();
            if (cmOpt.isPresent()) {
                String className = cmOpt.get().getClazz();
                try {
                    Class clazz = Class.forName(className);
                    Object object = clazz.newInstance();
                    return object;
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                    LOGGER.error("[MockController] 类加载异常,{}", e);
                }
            }

            //没有匹配到html,js,css,图片资源或者接口路径
            return null;
        }




    针对静态文件的处理模块,这里面主要是由responseHandle函数处理。


    代码如下:


     /**
         * 静态文件处理器
         *
         * @param object
         * @return
         * @throws IOException
         */

        public static Object responseHandle(Object object, ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException {
            String result;
            FullHttpResponse response = null;
            //接口的404处理模块
            if (object == null) {
                result = CommonUtil.read404Html();
                return ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);

            } else if (object instanceof JsModel) {

                JsModel jsModel = (JsModel) object;
                result = CommonUtil.readFileFromResource(jsModel.getUrl());
                response = notFoundHandler(result);
                return (response == null) ? ResponCoreHandle.responseJs(HttpResponseStatus.OK, result) : response;

            } else if (object instanceof CssModel) {

                CssModel cssModel = (CssModel) object;
                result = CommonUtil.readFileFromResource(cssModel.getUrl());
                response = notFoundHandler(result);
                return (response == null) ? ResponCoreHandle.responseCss(HttpResponseStatus.OK, result) : response;

            }//初始化页面
            else if (object instanceof PageModel) {

                PageModel pageModel = (PageModel) object;
                if (pageModel.getCode() == RequestConstants.INDEX_CODE) {
                    result = CommonUtil.readIndexHtml(pageModel.getPagePath());
                } else {
                    result = CommonUtil.readFileFromResource(pageModel.getPagePath());
                }

                return ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);

            } else if (object instanceof PicModel) {
                PicModel picModel = (PicModel) object;
                ResponCoreHandle.writePic(picModel.getUrl(), ctx, fullHttpRequest);
                return picModel;
            }
            return null;

        }

    对于接口类型的数据请求,主要是在handler里面完成


    代码为:


     if (!(response instanceof FullHttpResponse) && !(response instanceof PicModel)) {

                //接口处理
                if (isContaionInterFace(object, BaseController.class)) {
                    ControllerCglib cc = new ControllerCglib();
                    Object proxyObj = cc.getTarget(object);
                    Method[] methodArr = null;
                    Method aimMethod = null;


                    if (fullHttpRequest.method().equals(HttpMethod.GET)) {
                        methodArr = proxyObj.getClass().getMethods();
                        aimMethod = getMethodByName(methodArr, "doGet");
                    } else if (fullHttpRequest.method().equals(HttpMethod.POST)) {
                        methodArr = proxyObj.getClass().getMethods();
                        aimMethod = getMethodByName(methodArr, "doPost");
                    }

                    //代理执行method
                    if (aimMethod != null) {
                        ControllerRequest controllerRequest=paramterHandler(fullHttpRequest);
                        preHandler(controllerRequest);
                        BaseResponse baseResponse = (BaseResponse) aimMethod.invoke(proxyObj, controllerRequest);
                        aftHandler(controllerRequest);
                        result = JSON.toJSONString(baseResponse);
                    }
                }
                response = ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);
            }
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }

    这里面主要是借用了cglib来进行一些相关的代理编写,通过url找到匹配的controller,然后根据请求的类型来执行doget或者dopost功能。而preHandler和afterHandler主要是用于进行相关过滤器的执行操作。这里面用到了责任链的模式来进行编写。

    过滤链在程序初始化的时候便有进行相应的扫描和排序操作,核心代码思路如下所示:

        /**
         * 扫描过滤器
         *
         * @param path
         * @return
         */
        public static List<FilterModel> scanFilter(String path) throws IllegalAccessException, InstantiationException {
            Map<String, Object> result = new HashMap<>(60);
            Set<Class<?>> clazz = ClassUtil.getClzFromPkg(path);
            List<FilterModel> filterModelList = new ArrayList<>();
            for (Class<?> aClass : clazz) {
                if (aClass.isAnnotationPresent(Filter.class)) {
                    Filter filter = aClass.getAnnotation(Filter.class);
                    FilterModel filterModel = new FilterModel(filter.order(), filter.name(), aClass.newInstance());
                    filterModelList.add(filterModel);
                }
            }
            FilterModel[] tempArr = new FilterModel[filterModelList.size()];
            int index = 0;
            for (FilterModel filterModel : filterModelList) {
                tempArr[index] = filterModel;
                System.out.println("[Filter] " + filterModel.toString());
                index++;
            }
            return sortFilterModel(tempArr);
        }

        /**
         * 对加载的filter进行优先级排序
         *
         * @return
         */
        private static List<FilterModel> sortFilterModel(FilterModel[] filterModels) {
            for (int i = 0; i < filterModels.length; i++) {
                int minOrder = filterModels[i].getOrder();
                int minIndex = i;
                for (int j = i; j < filterModels.length; j++) {
                    if (minOrder > filterModels[j].getOrder()) {
                        minOrder = filterModels[j].getOrder();
                        minIndex = j;
                    }
                }
                FilterModel temp = filterModels[minIndex];
                filterModels[minIndex] = filterModels[i];
                filterModels[i] = temp;
            }
            return Arrays.asList(filterModels);
        }

    最后附上本框架的码云地址:

    https://gitee.com/IdeaHome_admin/ITree

    内附对应的源代码,jar包,以及可以让人理解思路的代码注释,喜欢的朋友可以给个star。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存